#!/usr/bin/env python
## Copyright (C) 2010, Scott W. Dunlop <swdunlop@gmail.com>
## All rights reserved.
## 
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##    * Redistributions of source code must retain the above copyright
##      notice, this list of conditions and the following disclaimer.
##    * Redistributions in binary form must reproduce the above copyright
##      notice, this list of conditions and the following disclaimer in the
##      documentation and/or other materials provided with the distribution.
##    * Neither the name of the <organization> nor the
##      names of its contributors may be used to endorse or promote products
##      derived from this software without specific prior written permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
## AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
## IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
## ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
## DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
NoGo is a very high level wrapper around the NoSpex graphing library
that presents users with a way to incrementally construct a graph
without bothering with ubiquitous concerns like thread safety and event 
models.

The following key requirements and assumptions are made:
    * There is only one graph, which NoGo created and will manage.
    * Every node has an identifier.
    * No two nodes have the same identifier.
    * There is only one link / edge connecting two nodes.
    * Nodes should be created when referenced if they did not exist.
"""

__all__ = [
  "control", "node", "link", "blip", # Key constructors and elements.
  "is_node", "is_link", "is_ident",  # Useful predicates.
  "spawn", "shell"                   # I hate having to define this, GvR..
]

import threading, subprocess
from nospex import NoSpexGraph, NoSpexNode, NoSpexLink, NoSpexBlip, control, FROM_SRC, FROM_DST

graph = NoSpexGraph( )

nodes_by_identifier = { }

class NoGoResource( object ):
    """
    Abstract behavior mixin that handles some of the behavior expected by nogo
    consumers.
    """

    def __init__( self, ident = None, desc = None ):
        self._ident = ident
        self._desc = desc
        self._click = None

    @property
    def ident( self ):
        "the node's unique identifier, assigned at creation"
        with control: return self._ident
    
    @property
    def desc( self ):
        "description for the node presented on mouseover"
        return self._desc

    @property
    def click( self ):
        "the function called when the node is clicked" 
        with control: return self._click

    @click.setter
    def click( self, value ):
        #TODO: Validate arity of the value.
        with control: self._click = value

    def handleEnter( self ):
        with control: self.info = str( self._desc or self._ident )
            
    def handleLeave( self ):
        with control: del self.info

    def handleClick( self ):
        with control: 
            if self._click is not None: return self._click( self.ident )
    
class NoGoLink( NoGoResource, NoSpexLink ):
    def __init__( self, src, dst ):
        "should not be called directly, requires control"
        NoSpexLink.__init__( self, src, dst )
        self.src = src
        self.dst = dst
        src._links_by_node[dst] = self
        dst._links_by_node[src] = self
        graph.requestPhysics( )

    def handleUnlink( self ):
        with control:
            if (self.src is not None) and (self.dst is not None):
                del self.src._links_by_node[ self.dst ]
                del self.dst._links_by_node[ self.src ]
            
            self.src = None
            self.dst = None

    def blip( self, other = None, time = 1000 ):
        with control:  #TODO: Remove _blip indirection.
            if other is None: other = self.destination
            return NoGoBlip( 
                self, time, FROM_DST if self.source == other else FROM_SRC 
            )
    
class NoGoNode( NoGoResource, NoSpexNode ):
    def __init__( self, ident ):
        "should not be called directly, requires control"
        NoSpexNode.__init__( self, graph )
        NoGoResource.__init__( self, ident )

        nodes_by_identifier[ ident ] = self
        self._links_by_node = {}
        graph.requestPhysics( )
        
    def handleUnlink( self ):
        with control: del nodes_by_identifier[ self.ident ]
                    
    def link( self, other ):
        "identifies/establishes a link to another node"
        with control: #TODO: Remove _link indirection.
            other = node( other )
            result = self._links_by_node.get( other )
            if result is not None: return result
            result = other._links_by_node.get( self )
            if result is not None: return result
            return NoGoLink( self, other )
    
class NoGoBlip( NoGoResource, NoSpexBlip ):
    def __init__( self, *args ):
        NoSpexBlip.__init__( self, *args )

def is_node( value ):
    "returns true if the value is a NoGo node"
    return isinstance( value, NoGoNode )

def is_ident( value ):
    "returns true if the value is a NoGo node or references one"

    if is_node( value ): return True
    with control: return nodes_by_identifier.has_key( ident )

def _node( ident ):
    "uncontrolled variation of node"
    if is_node( ident ): return ident
    return nodes_by_identifier.get( ident ) or NoGoNode( ident )

def node( ident ):
    "resolves an existing node or creates a new node"
    with control: return _node( ident )

def _link( src, dst ):
    "uncontrolled variation of link"
    return _node( src ).link( dst )

def link( src, dst ):
    "resolves an existing link from src to dst"
    with control: return _link( src, dst )

def _blip( src, dst, time = 1000 ):
    "uncontrolled variation of blip"
    return _link( src, dst ).blip( dst )

def blip( src, dst, time = 1000 ):
    "resolves an existing blip from src to dst"
    with control: return _blip( src, dst, time )

def spawn( func ):
    "spawns a thunk into its own thread and starts it"
    t = threading.Thread( target=func )
    t.start( )
    return t

def shell( *cmd ):
    "invokes a shell command and waits, returns the return code and output"
    proc = subprocess.Popen( cmd, stdout=subprocess.PIPE )
    stdout, stderr = proc.communicate( )
    return proc.returncode, stdout

# We put the graph into the background on import.
spawn( graph.display )

